/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.authenticator;

import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;

import org.apache.catalina.Authenticator;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Manager;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.util.DateTool;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.catalina.util.StringManager;
import org.apache.catalina.valves.ValveBase;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Basic implementation of the <b>Valve</b> interface that enforces the
 * <code>&lt;security-constraint&gt;</code> elements in the web application
 * deployment descriptor. This functionality is implemented as a Valve so that
 * it can be ommitted in environments that do not require these features.
 * Individual implementations of each supported authentication method can
 * subclass this base class as required.
 * <p>
 * <b>USAGE CONSTRAINT</b>: When this class is utilized, the Context to which it
 * is attached (or a parent Container in a hierarchy) must have an associated
 * Realm that can be used for authenticating users and enumerating the roles to
 * which they have been assigned.
 * <p>
 * <b>USAGE CONSTRAINT</b>: This Valve is only useful when processing HTTP
 * requests. Requests of any other type will simply be passed through.
 * 
 * @author Craig R. McClanahan
 * @version $Id: AuthenticatorBase.java 1135544 2011-06-14 13:04:43Z markt $
 */

public abstract class AuthenticatorBase extends ValveBase implements
		Authenticator, Lifecycle {
	private static Log log = LogFactory.getLog(AuthenticatorBase.class);

	// ----------------------------------------------------- Instance Variables

	/**
	 * The default message digest algorithm to use if we cannot use the
	 * requested one.
	 */
	protected static final String DEFAULT_ALGORITHM = "MD5";

	/**
	 * The number of random bytes to include when generating a session
	 * identifier.
	 */
	protected static final int SESSION_ID_BYTES = 16;

	/**
	 * Default authentication realm name.
	 */
	protected static final String REALM_NAME = "Authentication required";

	/**
	 * The message digest algorithm to be used when generating session
	 * identifiers. This must be an algorithm supported by the
	 * <code>java.security.MessageDigest</code> class on your platform.
	 */
	protected String algorithm = DEFAULT_ALGORITHM;

	/**
	 * Should we cache authenticated Principals if the request is part of an
	 * HTTP session?
	 */
	protected boolean cache = true;

	/**
	 * Should the session ID, if any, be changed upon a successful
	 * authentication to prevent a session fixation attack?
	 */
	protected boolean changeSessionIdOnAuthentication = true;

	/**
	 * The Context to which this Valve is attached.
	 */
	protected Context context = null;

	/**
	 * Return the MessageDigest implementation to be used when creating session
	 * identifiers.
	 */
	protected MessageDigest digest = null;

	/**
	 * A String initialization parameter used to increase the entropy of the
	 * initialization of our random number generator.
	 */
	protected String entropy = null;

	/**
	 * Descriptive information about this implementation.
	 */
	protected static final String info = "org.apache.catalina.authenticator.AuthenticatorBase/1.0";

	/**
	 * Flag to determine if we disable proxy caching, or leave the issue up to
	 * the webapp developer.
	 */
	protected boolean disableProxyCaching = true;

	/**
	 * Flag to determine if we disable proxy caching with headers incompatible
	 * with IE
	 */
	protected boolean securePagesWithPragma = true;

	/**
	 * The lifecycle event support for this component.
	 */
	protected LifecycleSupport lifecycle = new LifecycleSupport(this);

	/**
	 * A random number generator to use when generating session identifiers.
	 */
	protected Random random = null;

	/**
	 * The Java class name of the random number generator class to be used when
	 * generating session identifiers.
	 */
	protected String randomClass = "java.security.SecureRandom";

	/**
	 * The string manager for this package.
	 */
	protected static final StringManager sm = StringManager
			.getManager(Constants.Package);

	/**
	 * The SingleSignOn implementation in our request processing chain, if there
	 * is one.
	 */
	protected SingleSignOn sso = null;

	/**
	 * Has this component been started?
	 */
	protected boolean started = false;

	/**
	 * "Expires" header always set to Date(1), so generate once only
	 */
	private static final String DATE_ONE = (new SimpleDateFormat(
			DateTool.HTTP_RESPONSE_DATE_HEADER, Locale.US)).format(new Date(1));

	// ------------------------------------------------------------- Properties

	/**
	 * Return the message digest algorithm for this Manager.
	 */
	public String getAlgorithm() {

		return (this.algorithm);

	}

	/**
	 * Set the message digest algorithm for this Manager.
	 * 
	 * @param algorithm
	 *            The new message digest algorithm
	 */
	public void setAlgorithm(String algorithm) {

		this.algorithm = algorithm;

	}

	/**
	 * Return the cache authenticated Principals flag.
	 */
	public boolean getCache() {

		return (this.cache);

	}

	/**
	 * Set the cache authenticated Principals flag.
	 * 
	 * @param cache
	 *            The new cache flag
	 */
	public void setCache(boolean cache) {

		this.cache = cache;

	}

	/**
	 * Return the Container to which this Valve is attached.
	 */
	public Container getContainer() {

		return (this.context);

	}

	/**
	 * Set the Container to which this Valve is attached.
	 * 
	 * @param container
	 *            The container to which we are attached
	 */
	public void setContainer(Container container) {

		if (container != null && !(container instanceof Context))
			throw new IllegalArgumentException(sm
					.getString("authenticator.notContext"));

		super.setContainer(container);
		this.context = (Context) container;

	}

	/**
	 * Return the entropy increaser value, or compute a semi-useful value if
	 * this String has not yet been set.
	 */
	public String getEntropy() {

		// Calculate a semi-useful value if this has not been set
		if (this.entropy == null)
			setEntropy(this.toString());

		return (this.entropy);

	}

	/**
	 * Set the entropy increaser value.
	 * 
	 * @param entropy
	 *            The new entropy increaser value
	 */
	public void setEntropy(String entropy) {

		this.entropy = entropy;

	}

	/**
	 * Return descriptive information about this Valve implementation.
	 */
	public String getInfo() {

		return (info);

	}

	/**
	 * Return the random number generator class name.
	 */
	public String getRandomClass() {

		return (this.randomClass);

	}

	/**
	 * Set the random number generator class name.
	 * 
	 * @param randomClass
	 *            The new random number generator class name
	 */
	public void setRandomClass(String randomClass) {

		this.randomClass = randomClass;

	}

	/**
	 * Return the flag that states if we add headers to disable caching by
	 * proxies.
	 */
	public boolean getDisableProxyCaching() {
		return disableProxyCaching;
	}

	/**
	 * Set the value of the flag that states if we add headers to disable
	 * caching by proxies.
	 * 
	 * @param nocache
	 *            <code>true</code> if we add headers to disable proxy caching,
	 *            <code>false</code> if we leave the headers alone.
	 */
	public void setDisableProxyCaching(boolean nocache) {
		disableProxyCaching = nocache;
	}

	/**
	 * Return the flag that states, if proxy caching is disabled, what headers
	 * we add to disable the caching.
	 */
	public boolean getSecurePagesWithPragma() {
		return securePagesWithPragma;
	}

	/**
	 * Set the value of the flag that states what headers we add to disable
	 * proxy caching.
	 * 
	 * @param securePagesWithPragma
	 *            <code>true</code> if we add headers which are incompatible
	 *            with downloading office documents in IE under SSL but which
	 *            fix a caching problem in Mozilla.
	 */
	public void setSecurePagesWithPragma(boolean securePagesWithPragma) {
		this.securePagesWithPragma = securePagesWithPragma;
	}

	/**
	 * Return the flag that states if we should change the session ID of an
	 * existing session upon successful authentication.
	 * 
	 * @return <code>true</code> to change session ID upon successful
	 *         authentication, <code>false</code> to do not perform the change.
	 */
	public boolean getChangeSessionIdOnAuthentication() {
		return changeSessionIdOnAuthentication;
	}

	/**
	 * Set the value of the flag that states if we should change the session ID
	 * of an existing session upon successful authentication.
	 * 
	 * @param changeSessionIdOnAuthentication
	 *            <code>true</code> to change session ID upon successful
	 *            authentication, <code>false</code> to do not perform the
	 *            change.
	 */
	public void setChangeSessionIdOnAuthentication(
			boolean changeSessionIdOnAuthentication) {
		this.changeSessionIdOnAuthentication = changeSessionIdOnAuthentication;
	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Enforce the security restrictions in the web application deployment
	 * descriptor of our associated Context.
	 * 
	 * @param request
	 *            Request to be processed
	 * @param response
	 *            Response to be processed
	 * 
	 * @exception IOException
	 *                if an input/output error occurs
	 * @exception ServletException
	 *                if thrown by a processing element
	 */
	public void invoke(Request request, Response response) throws IOException,
			ServletException {

		if (log.isDebugEnabled())
			log.debug("Security checking request " + request.getMethod() + " "
					+ request.getRequestURI());
		LoginConfig config = this.context.getLoginConfig();

		// Have we got a cached authenticated Principal to record?
		if (cache) {
			Principal principal = request.getUserPrincipal();
			if (principal == null) {
				Session session = request.getSessionInternal(false);
				if (session != null) {
					principal = session.getPrincipal();
					if (principal != null) {
						if (log.isDebugEnabled())
							log.debug("We have cached auth type "
									+ session.getAuthType() + " for principal "
									+ session.getPrincipal());
						request.setAuthType(session.getAuthType());
						request.setUserPrincipal(principal);
					}
				}
			}
		}

		// Special handling for form-based logins to deal with the case
		// where the login form (and therefore the "j_security_check" URI
		// to which it submits) might be outside the secured area
		String contextPath = this.context.getPath();
		String requestURI = request.getDecodedRequestURI();
		if (requestURI.startsWith(contextPath)
				&& requestURI.endsWith(Constants.FORM_ACTION)) {
			if (!authenticate(request, response, config)) {
				if (log.isDebugEnabled())
					log.debug(" Failed authenticate() test ??" + requestURI);
				return;
			}
		}

		Realm realm = this.context.getRealm();
		// Is this request URI subject to a security constraint?
		SecurityConstraint[] constraints = realm.findSecurityConstraints(
				request, this.context);

		if ((constraints == null) /*
								 * &&(!Constants.FORM_METHOD.equals(config.
								 * getAuthMethod()))
								 */) {
			if (log.isDebugEnabled())
				log.debug(" Not subject to any constraint");
			getNext().invoke(request, response);
			return;
		}

		// Make sure that constrained resources are not cached by web proxies
		// or browsers as caching can provide a security hole
		if (disableProxyCaching &&
		// FIXME: Disabled for Mozilla FORM support over SSL
				// (improper caching issue)
				// !request.isSecure() &&
				!"POST".equalsIgnoreCase(request.getMethod())) {
			if (securePagesWithPragma) {
				// FIXME: These cause problems with downloading office docs
				// from IE under SSL and may not be needed for newer Mozilla
				// clients.
				response.setHeader("Pragma", "No-cache");
				response.setHeader("Cache-Control", "no-cache");
			} else {
				response.setHeader("Cache-Control", "private");
			}
			response.setHeader("Expires", DATE_ONE);
		}

		int i;
		// Enforce any user data constraint for this security constraint
		if (log.isDebugEnabled()) {
			log.debug(" Calling hasUserDataPermission()");
		}
		if (!realm.hasUserDataPermission(request, response, constraints)) {
			if (log.isDebugEnabled()) {
				log.debug(" Failed hasUserDataPermission() test");
			}
			/*
			 * ASSERT: Authenticator already set the appropriate HTTP status
			 * code, so we do not have to do anything special
			 */
			return;
		}

		// Since authenticate modifies the response on failure,
		// we have to check for allow-from-all first.
		boolean authRequired = true;
		for (i = 0; i < constraints.length && authRequired; i++) {
			if (!constraints[i].getAuthConstraint()) {
				authRequired = false;
			} else if (!constraints[i].getAllRoles()) {
				String[] roles = constraints[i].findAuthRoles();
				if (roles == null || roles.length == 0) {
					authRequired = false;
				}
			}
		}

		if (authRequired) {
			if (log.isDebugEnabled()) {
				log.debug(" Calling authenticate()");
			}
			if (!authenticate(request, response, config)) {
				if (log.isDebugEnabled()) {
					log.debug(" Failed authenticate() test");
				}
				/*
				 * ASSERT: Authenticator already set the appropriate HTTP status
				 * code, so we do not have to do anything special
				 */
				return;
			}

		}

		if (log.isDebugEnabled()) {
			log.debug(" Calling accessControl()");
		}
		if (!realm.hasResourcePermission(request, response, constraints,
				this.context)) {
			if (log.isDebugEnabled()) {
				log.debug(" Failed accessControl() test");
			}
			/*
			 * ASSERT: AccessControl method has already set the appropriate HTTP
			 * status code, so we do not have to do anything special
			 */
			return;
		}

		// Any and all specified constraints have been satisfied
		if (log.isDebugEnabled()) {
			log.debug(" Successfully passed all security constraints");
		}
		getNext().invoke(request, response);

	}

	// ------------------------------------------------------ Protected Methods

	/**
	 * Associate the specified single sign on identifier with the specified
	 * Session.
	 * 
	 * @param ssoId
	 *            Single sign on identifier
	 * @param session
	 *            Session to be associated
	 */
	protected void associate(String ssoId, Session session) {

		if (sso == null)
			return;
		sso.associate(ssoId, session);

	}

	/**
	 * Authenticate the user making this request, based on the specified login
	 * configuration. Return <code>true</code> if any specified constraint has
	 * been satisfied, or <code>false</code> if we have created a response
	 * challenge already.
	 * 
	 * @param request
	 *            Request we are processing
	 * @param response
	 *            Response we are creating
	 * @param config
	 *            Login configuration describing how authentication should be
	 *            performed
	 * 
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	protected abstract boolean authenticate(Request request, Response response,
			LoginConfig config) throws IOException;

	/**
	 * Generate and return a new session identifier for the cookie that
	 * identifies an SSO principal.
	 */
	protected synchronized String generateSessionId() {

		// Generate a byte array containing a session identifier
		byte bytes[] = new byte[SESSION_ID_BYTES];
		getRandom().nextBytes(bytes);
		bytes = getDigest().digest(bytes);

		// Render the result as a String of hexadecimal digits
		StringBuffer result = new StringBuffer();
		for (int i = 0; i < bytes.length; i++) {
			byte b1 = (byte) ((bytes[i] & 0xf0) >> 4);
			byte b2 = (byte) (bytes[i] & 0x0f);
			if (b1 < 10)
				result.append((char) ('0' + b1));
			else
				result.append((char) ('A' + (b1 - 10)));
			if (b2 < 10)
				result.append((char) ('0' + b2));
			else
				result.append((char) ('A' + (b2 - 10)));
		}
		return (result.toString());

	}

	/**
	 * Return the MessageDigest object to be used for calculating session
	 * identifiers. If none has been created yet, initialize one the first time
	 * this method is called.
	 */
	protected synchronized MessageDigest getDigest() {

		if (this.digest == null) {
			try {
				this.digest = MessageDigest.getInstance(algorithm);
			} catch (NoSuchAlgorithmException e) {
				try {
					this.digest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
				} catch (NoSuchAlgorithmException f) {
					this.digest = null;
				}
			}
		}

		return (this.digest);

	}

	/**
	 * Return the random number generator instance we should use for generating
	 * session identifiers. If there is no such generator currently defined,
	 * construct and seed a new one.
	 */
	protected synchronized Random getRandom() {

		if (this.random == null) {
			try {
				Class clazz = Class.forName(randomClass);
				this.random = (Random) clazz.newInstance();
				long seed = System.currentTimeMillis();
				char entropy[] = getEntropy().toCharArray();
				for (int i = 0; i < entropy.length; i++) {
					long update = ((byte) entropy[i]) << ((i % 8) * 8);
					seed ^= update;
				}
				this.random.setSeed(seed);
			} catch (Exception e) {
				this.random = new java.util.Random();
			}
		}

		return (this.random);

	}

	/**
	 * Attempts reauthentication to the <code>Realm</code> using the credentials
	 * included in argument <code>entry</code>.
	 * 
	 * @param ssoId
	 *            identifier of SingleSignOn session with which the caller is
	 *            associated
	 * @param request
	 *            the request that needs to be authenticated
	 */
	protected boolean reauthenticateFromSSO(String ssoId, Request request) {

		if (sso == null || ssoId == null)
			return false;

		boolean reauthenticated = false;

		Container parent = getContainer();
		if (parent != null) {
			Realm realm = parent.getRealm();
			if (realm != null) {
				reauthenticated = sso.reauthenticate(ssoId, realm, request);
			}
		}

		if (reauthenticated) {
			associate(ssoId, request.getSessionInternal(true));

			if (log.isDebugEnabled()) {
				log.debug(" Reauthenticated cached principal '"
						+ request.getUserPrincipal().getName()
						+ "' with auth type '" + request.getAuthType() + "'");
			}
		}

		return reauthenticated;
	}

	/**
	 * Register an authenticated Principal and authentication type in our
	 * request, in the current session (if there is one), and with our
	 * SingleSignOn valve, if there is one. Set the appropriate cookie to be
	 * returned.
	 * 
	 * @param request
	 *            The servlet request we are processing
	 * @param response
	 *            The servlet response we are generating
	 * @param principal
	 *            The authenticated Principal to be registered
	 * @param authType
	 *            The authentication type to be registered
	 * @param username
	 *            Username used to authenticate (if any)
	 * @param password
	 *            Password used to authenticate (if any)
	 */
	protected void register(Request request, Response response,
			Principal principal, String authType, String username,
			String password) {

		if (log.isDebugEnabled()) {
			String name = (principal == null) ? "none" : principal.getName();
			log.debug("Authenticated '" + name + "' with type '" + authType
					+ "'");
		}

		// Cache the authentication information in our request
		request.setAuthType(authType);
		request.setUserPrincipal(principal);

		Session session = request.getSessionInternal(false);

		if (session != null && changeSessionIdOnAuthentication) {
			Manager manager = request.getContext().getManager();
			manager.changeSessionId(session);
			request.changeSessionId(session.getId());
		}

		// Cache the authentication information in our session, if any
		if (cache) {
			if (session != null) {
				session.setAuthType(authType);
				session.setPrincipal(principal);
				if (username != null)
					session.setNote(Constants.SESS_USERNAME_NOTE, username);
				else
					session.removeNote(Constants.SESS_USERNAME_NOTE);
				if (password != null)
					session.setNote(Constants.SESS_PASSWORD_NOTE, password);
				else
					session.removeNote(Constants.SESS_PASSWORD_NOTE);
			}
		}

		// Construct a cookie to be returned to the client
		if (sso == null)
			return;

		// Only create a new SSO entry if the SSO did not already set a note
		// for an existing entry (as it would do with subsequent requests
		// for DIGEST and SSL authenticated contexts)
		String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
		if (ssoId == null) {
			// Construct a cookie to be returned to the client
			ssoId = generateSessionId();
			Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE, ssoId);
			cookie.setMaxAge(-1);
			cookie.setPath("/");

			// Bugzilla 41217
			cookie.setSecure(request.isSecure());

			// Bugzilla 34724
			String ssoDomain = sso.getCookieDomain();
			if (ssoDomain != null) {
				cookie.setDomain(ssoDomain);
			}

			response.addCookieInternal(cookie, context.getUseHttpOnly());

			// Register this principal with our SSO valve
			sso.register(ssoId, principal, authType, username, password);
			request.setNote(Constants.REQ_SSOID_NOTE, ssoId);

		} else {
			// Update the SSO session with the latest authentication data
			sso.update(ssoId, principal, authType, username, password);
		}

		// Fix for Bug 10040
		// Always associate a session with a new SSO reqistration.
		// SSO entries are only removed from the SSO registry map when
		// associated sessions are destroyed; if a new SSO entry is created
		// above for this request and the user never revisits the context, the
		// SSO entry will never be cleared if we don't associate the session
		if (session == null)
			session = request.getSessionInternal(true);
		sso.associate(ssoId, session);

	}

	// ------------------------------------------------------ Lifecycle Methods

	/**
	 * Add a lifecycle event listener to this component.
	 * 
	 * @param listener
	 *            The listener to add
	 */
	public void addLifecycleListener(LifecycleListener listener) {

		lifecycle.addLifecycleListener(listener);

	}

	/**
	 * Get the lifecycle listeners associated with this lifecycle. If this
	 * Lifecycle has no listeners registered, a zero-length array is returned.
	 */
	public LifecycleListener[] findLifecycleListeners() {

		return lifecycle.findLifecycleListeners();

	}

	/**
	 * Remove a lifecycle event listener from this component.
	 * 
	 * @param listener
	 *            The listener to remove
	 */
	public void removeLifecycleListener(LifecycleListener listener) {

		lifecycle.removeLifecycleListener(listener);

	}

	/**
	 * Prepare for the beginning of active use of the public methods of this
	 * component. This method should be called after <code>configure()</code>,
	 * and before any of the public methods of the component are utilized.
	 * 
	 * @exception LifecycleException
	 *                if this component detects a fatal error that prevents this
	 *                component from being used
	 */
	public void start() throws LifecycleException {

		// Validate and update our current component state
		if (started)
			throw new LifecycleException(sm
					.getString("authenticator.alreadyStarted"));
		lifecycle.fireLifecycleEvent(START_EVENT, null);
		started = true;

		// Look up the SingleSignOn implementation in our request processing
		// path, if there is one
		Container parent = context.getParent();
		while ((sso == null) && (parent != null)) {
			if (!(parent instanceof Pipeline)) {
				parent = parent.getParent();
				continue;
			}
			Valve valves[] = ((Pipeline) parent).getValves();
			for (int i = 0; i < valves.length; i++) {
				if (valves[i] instanceof SingleSignOn) {
					sso = (SingleSignOn) valves[i];
					break;
				}
			}
			if (sso == null)
				parent = parent.getParent();
		}
		if (log.isDebugEnabled()) {
			if (sso != null)
				log.debug("Found SingleSignOn Valve at " + sso);
			else
				log.debug("No SingleSignOn Valve is present");
		}

	}

	/**
	 * Gracefully terminate the active use of the public methods of this
	 * component. This method should be the last one called on a given instance
	 * of this component.
	 * 
	 * @exception LifecycleException
	 *                if this component detects a fatal error that needs to be
	 *                reported
	 */
	public void stop() throws LifecycleException {

		// Validate and update our current component state
		if (!started)
			throw new LifecycleException(sm
					.getString("authenticator.notStarted"));
		lifecycle.fireLifecycleEvent(STOP_EVENT, null);
		started = false;

		sso = null;

	}

}
